Parser.php

<?php

namespace Phad\Test\Compilation;

class Parser extends \Phad\Tester {

    /**
     * @param $code code to `eval()`. 
     * @param $args args to `extract()` prior to `eval($code)`
     * @param $expect the expected output from `eval($code)`
     *
     * @note prefix $code with `?>` if the $code has its own php open tags
     */
    public function run_test_exec(string $code, array $args, string $expect){

        extract($args);

        // $_node = ['index'=>0];
        $_index = isset($args['TestItem']) ? $args['TestItem']->data_index : null;
        // var_dump($code);
        // exit;
        try {
            ob_start();
            eval($code);
            $out = ob_get_clean();
            $this->compare_lines($expect,$out);
        } catch (\ParseError $e){
            ob_end_clean();
            echo "Parse error in eval'd code! You may want to prefix the code with `?>` so it's in html mode, not php mode";
            echo "\n\n";
            throw $e;
        }     }
    /**
     *
     * Test a view with a single item. I don't even know how multiple items works yet.
     * @return the PHTML doc used in parsing
     */
    public function run_parse_test(string $view, string $target_html, array $var_exports, array $target_item_data){

        $parser = new \Phad\DomParser();
        $doc = new \Taeluf\PHTML($view);
        $items = $parser->parse_doc($doc);
        $item = $items[0];

        // print_r($items[0]['html_code']);
        // exit;

        $this->compare_lines($target_html, $item['html_code']);
        unset($item['html_code']);

        // print_r($item);

        $var_exports = array_map(function($v){return var_export($v,true);},$var_exports);
        $target_data = array_merge($target_item_data, $var_exports);

        
        $actual_data = [];
        foreach ($target_data as $key=>$value){
            if (!isset($item[$key]))continue;
            $actual_data[$key] = $item[$key];
        }

        $this->compare($target_data, $actual_data);
        return $doc;
    }

    public function compare_data(array $actual, array $target_data, array $target_data_var_exports){
        $var_exports = $target_data_var_exports;
        $var_exports = array_map(function($v){return var_export($v,true);}, $var_exports);
        $target_data = array_merge($target_data, $var_exports);
//
        // print_r($item_data);
        // print_r($target_data);
        // exit;
        $actual_data = [];
        foreach ($target_data as $key=>$value){
            $actual_data[$key] = $actual[$key];
        }

        $this->compare($target_data, $actual_data);

    }

    /**
     * @test doc output contains filter call
     */
    public function testPropertyFilter(){
        $view = <<<HTML
            <div item="Blog">
                <h1 prop="title" filter="test:whocares"></h1>
            </div>
        HTML;
        $expect = <<<HTML
            <div>
                <h1><?=\$phad->filter('test:whocares',\$Blog->title)?></h1>
            </div>
        HTML;
        $parser = new \Phad\DomParser();
        $doc = new \Taeluf\PHTML($view);
        $template_args = $parser->parse_doc($doc);
        // echo $doc;
        $this->compare_lines($expect, $template_args[0]['html_code']);
    }

    /**
     * @test doc output for a single node with access attribute prior to an item node
     */
    public function testAccessAttributeBeforeItem(){
        $view = <<<HTML
            <div access="role:cat">
                <?php echo "that is how a cat do"; ?>
            </div>
            <div item="Blog">
                <h1 prop="title"></h1>
            </div>
        HTML;
        $parser = new \Phad\DomParser();
        $doc = new \Taeluf\PHTML($view);
        $template_args = $parser->parse_doc($doc);
        echo $doc;
        $this->str_contains(
            $doc,
            '<?php if ($phad->can_read_node(array (',
            '<?php echo "that is how a cat do"; ?>',
            '<h1><?=$Blog->title?></h1>',
        );
    }

    /**
     * @test doc output for a single node with access attribute
     */
    public function testAccessAttribute(){
        $view = <<<HTML
            <div access="role:cat">
                <?php echo "that is how a cat do"; ?>
            </div>
        HTML;
        $expect = <<<HTML
            <?php
            if (\$phad_block===\Phad\Blocks::ROUTE_META){
                return array (
                );
            }
            ?><?php
            if ((\$phad_block??null)===\Phad\Blocks::SITEMAP_META){
                return array (
                );
            }
            ?>    <?php if (\$phad->can_read_node(array (
              'access' => 'role:cat',
              'tagName' => 'div',
            ))): ?>
            <div>
            <?php echo "that is how a cat do"; ?>
            </div>
            <?php else: \$phad->read_node_failed(array (
            'access' => 'role:cat',
            'tagName' => 'div',
            )); ?>
            <?php endif; ?>
        HTML;
        $parser = new \Phad\DomParser();
        $doc = new \Taeluf\PHTML($view);
        $template_args = $parser->parse_doc($doc);
        // print_r($template_args);
        // echo $doc;

        $this->compare_lines($expect, $doc.'');
    }
    /**
     * @test template_args for form
     * @test main code for form (string lines comparison)
     * @test executing form code
     */
    public function testForm(){
        $view = <<<HTML
            <form item="Blog">
                <input type="text" name="title">
            </form>
        HTML;
        $expect = <<<HTML
            <form action="" method="POST">
                <input type="text" name="title" value="<?=\$Blog->title?>">
            </form>
        HTML;


        $as_is = [
            'item_name' => 'Blog',
            'item_type' => 'form',
            'ItemForeach' => 'Blog => $BlogRow',
            'ItemName' => '$Blog',
            'Item_Row' => '$BlogRow',
            'ItemInfo' => '$BlogInfo',
            'form_submit'=>true,
            'form_properties'=>true,
            'form_submit'=>true,
        ];

        $exports = [
            'apis' => array (
              0 => 'form',
              1 => 'create',
              2 => 'update',
            ),
            'DataNodes' => array (
                  0 => 
                  array (
                    'type' => 'default',
                  ),
                ),
            'form_properties_array'=>[
                'title'=> ['type'=>'text','tagName'=>'input',],
                'id' => ['tagName'=>'input','type'=>'hidden'],
            ],
        ];

        $this->run_parse_test($view, $expect, $exports, $as_is);

        //////////
        /// exec tests
        //////////

        $parser = new \Phad\DomParser();
        $doc = new \Taeluf\PHTML($view);
        $items = $parser->parse_doc($doc);

        $item = $items[0];
        $this->run_test_exec('?>'.$item['html_code'], 
            ['Blog'=>(object)['title'=>'Good Title!']],
            <<<HTML
                <form action="" method="POST">
                    <input type="text" name="title" value="Good Title!">
                </form>
            HTML
        );
        // print_r($item);
    }

    public function testOn404_403(){
        // this mistakenly compiles code when it really should just check that the response code ... code is found in the compiled item

        $view = <<<HTML
            <div item="Blog">
                <p-data where="Blog.id = :id">
                    <on s=404><?php echo "404 error 1";?></on>
                    <on s=403><?php echo "403 error 1";?></on>
                </p-data>
                <p-data where="Blog.id = :id">
                    <on s=404><?php echo "404 error 2";?></on>
                    <on s=403><?php echo "403 error 2";?></on>
                </p-data>
                <h1 prop="title"></h1>
                <p prop="intro"></p>
            </div>
        HTML;
        $expect_html = <<<HTML
            <div>
                <h1><?=\$Blog->title?></h1>
                <p><?=\$Blog->intro?></p>
            </div>
        HTML;

        $compiler = new \Phad\TemplateCompiler();
        $out = $compiler->compile($view, $this->file('code/template/main.php'));
//
        // echo $out;
        // exit;

        $parser = new \Phad\DomParser();
        $doc = new \Taeluf\PHTML($view);
        $items = $parser->parse_doc($doc);

        $item = $items[0];
        // print_r($item);

        $this->run_test_exec(
            $item['code_for_response_403'],
            ['ItemInfo'=>'TestItem',
            'TestItem'=>(object)['data_index'=>0]
            ],
            '403 error 1',
        );
        $this->run_test_exec(
            $item['code_for_response_403'],
            ['ItemInfo'=>'TestItem',
            'TestItem'=>(object)['data_index'=>1]
            ],
            '403 error 2',
        );


        $this->run_test_exec(
            $item['code_for_response_404'],
            ['ItemInfo'=>'TestItem',
            'TestItem'=>(object)['data_index'=>0]
            ],
            '404 error 1',
        );
        $this->run_test_exec(
            $item['code_for_response_404'],
            ['ItemInfo'=>'TestItem',
            'TestItem'=>(object)['data_index'=>1]
            ],
            '404 error 2',
        );

        $this->run_test_exec(
            '?>'.$item['html_code'],
            [ 'Blog'=>(object)['title'=>'puppies', 'intro'=>'are cute'], ],
            <<<HTML
                <div>
                    <h1>puppies</h1>
                    <p>are cute</p>
                </div>
            HTML
        );
    }

    /**
     * @test data nodes
     * @test parsed output (where prop attributes are replaced by things like `<?=$Blog->title?>`
     * @test execution of 404 response code
     * @test execution of the main code (no compiler or phad object)
     */
    public function testOn404(){

        $view = <<<HTML
            <div item="Blog">
                <p-data where="Blog.id = :id">
                    <on s=404><?php echo "404 error";?></on>
                </p-data>
                <h1 prop="title"></h1>
                <p prop="intro"></p>
            </div>
        HTML;
        $expect_html = <<<HTML
            <div>
                <h1><?=\$Blog->title?></h1>
                <p><?=\$Blog->intro?></p>
            </div>
        HTML;


//         $compiler = new \Phad\TemplateCompiler();
//         $out = $compiler->compile($view, $this->file('code/template/main.php'));
// //
//         echo $out;
//         exit;

        $parser = new \Phad\DomParser();
        $doc = new \Taeluf\PHTML($view);
        $items = $parser->parse_doc($doc);

        $var_exports = 
        [
            'DataNodes'=>
            [
                ['where'=>'Blog.id = :id', 'type'=>'node'],
                ['type'=>'default']
            ],
        ];

        $items_data = [];

        $this->run_parse_test($view, $expect_html, $var_exports, $items_data);

        // print_r($items);

        $item = $items[0];

        $this->run_test_exec(
            $item['code_for_response_404'],
            ['ItemInfo'=>'TestItem',
            'TestItem'=>(object)['data_index'=>0],
            ],
            '404 error'
        );



        $this->run_test_exec(
            '?>'.$item['html_code'],
            [ 'Blog'=>(object)['title'=>'puppies', 'intro'=>'are cute'], ],
            <<<HTML
                <div>
                    <h1>puppies</h1>
                    <p>are cute</p>
                </div>
            HTML
        );

    }

    public function testNestedItem(){
        // pre-compile the author view so we can insert the compiled author into the expected blog copmiled output ... 
        $compiler = new \Phad\TemplateCompiler();
        $author_view = <<<HTML
                <div item="Author">
                    <p-data where="Author.id = :id"></p-data>
                    <h1 prop="name"></h1>
                    <p prop="hometown"></p>
                </div>
        HTML;


        // note the $author_view inserted into the blog template
        $view = <<<HTML
            <div item="Blog">
                <p-data where="Blog.id = :id"></p-data>
                <h1 prop="title"></h1>
                <p prop="intro"></p>
                {$author_view}
            </div>
        HTML;

        $full = $compiler->compile($view, file_get_contents($this->file('code/template/main.php')));

        $fully_compiled_Author = $compiler->compile($author_view, file_get_contents($this->file('code/template/main.php')));
        // i have to remove the ROUTE_META & SITEMAP_META portions since i've made those be added to every compiled view ...

        $fully_compiled_Author = explode('<?php', $fully_compiled_Author);
        // get the code before ROUTE_META's <?php
        $before = array_shift($fully_compiled_Author);
        // remove the ROUTE_META block
        array_shift($fully_compiled_Author);
        // remove the SITEMAP_META block
        array_shift($fully_compiled_Author);
        // re-insert the code before ROUTE_META's <?php
        array_unshift($fully_compiled_Author,$before);
        // put it back together as a string
        $fully_compiled_Author = implode('<?php', $fully_compiled_Author);


        $target_html = [
            //index 0 is author's html (just the item html, not the complicate template stuff)
            <<<HTML
                <div>
                    <h1><?=\$Author->name?></h1>
                    <p><?=\$Author->hometown?></p>
                </div>
            HTML,
            //index 1 is the blog html which contains the fully compiled author src 
            $expect_html = <<<HTML
                <div>
                    <h1><?=\$Blog->title?></h1>
                    <p><?=\$Blog->intro?></p>
                    $fully_compiled_Author
                </div>
            HTML,
        ];

        $parser = new \Phad\DomParser();
        $doc = new \Taeluf\PHTML($view);
        $items = $parser->parse_doc($doc);

        $var_exports = 
        [ 
            //author comes first, bc it is a child node, so it gets compiled first
            0=>// 'Author_nodes'=>
            [
                'DataNodes'=>[
                    ['where'=>'Author.id = :id', 'type'=>'node'],
                    ['type'=>'default'],
                ],
            ],
            1=>// 'Blog_Nodes'=>
            [
                'DataNodes'=>
                [
                    ['where'=>'Blog.id = :id', 'type'=>'node'],
                    ['type'=>'default']
                ],
            ],
        ];

        foreach ($items as $index=>$item_data){
            $exports = $var_exports[$index];
            $exports = array_map(function($v){return var_export($v,true);}, $exports);
            $this->test('compare lines');
            $this->compare_lines($target_html[$index], $item_data['html_code']);

            $this->test('compare data');
            $this->compare_data($item_data, [], $var_exports[$index]);
        }
        // echo $doc;

        $this->str_contains($doc,
            '$BlogInfo = (object)[',
            "'where' => 'Blog.id = :id'",
            '<h1><?=$Blog->title?></h1>',

            '$AuthorInfo = (object)[',
            "'where' => 'Author.id = :id',",
            '<p><?=$Author->hometown?></p>',
        );
    }

    /**
     * @test that doc output contains both items
     * @test that parser creates the correct template_args
     */
    public function testTwoItems(){
        $view = <<<HTML
            <div item="Blog">
                <p-data where="Blog.id = :id"></p-data>
                <h1 prop="title"></h1>
                <p prop="intro"></p>
            </div>
            <div item="Author">
                <p-data where="Author.id = :id"></p-data>
                <h1 prop="name"></h1>
                <p prop="hometown"></p>
            </div>
        HTML;

        $compiler = new \Phad\TemplateCompiler();
        $full = $compiler->compile($view, file_get_contents($this->file('code/template/main.php')));

        // var_dump($full);
        // exit;




        $target_html = [
            $expect_html = <<<HTML
                <div>
                    <h1><?=\$Blog->title?></h1>
                    <p><?=\$Blog->intro?></p>
                </div>
            HTML,
            <<<HTML
                <div>
                    <h1><?=\$Author->name?></h1>
                    <p><?=\$Author->hometown?></p>
                </div>
            HTML,
        ];

        $parser = new \Phad\DomParser();
        $doc = new \Taeluf\PHTML($view);
        $items = $parser->parse_doc($doc);

        $var_exports = 
        [ 
            0=>// 'Blog_Nodes'=>
            [
                'DataNodes'=>
                [
                    ['where'=>'Blog.id = :id', 'type'=>'node'],
                    ['type'=>'default']
                ],
            ],
            1=>// 'Author_nodes'=>
            [
                'DataNodes'=>[
                    ['where'=>'Author.id = :id', 'type'=>'node'],
                    ['type'=>'default'],
                ],
            ],
        ];

        foreach ($items as $index=>$item_data){
            $exports = $var_exports[$index];
            $exports = array_map(function($v){return var_export($v,true);}, $exports);
            $this->compare_lines($target_html[$index], $item_data['html_code']);

            $this->compare_data($item_data, [], $var_exports[$index]);
        }
        echo $doc;

        $this->str_contains($doc,
            '$BlogInfo = (object)[',
            "'where' => 'Blog.id = :id'",
            '<h1><?=$Blog->title?></h1>',

            '$AuthorInfo = (object)[',
            "'where' => 'Author.id = :id',",
            '<p><?=$Author->hometown?></p>',
        );
    }

    public function testInnerLoop(){
        $view = <<<HTML
            <div item="Blog" loop="inner">
                <h1 prop="title"></h1>
            </div>
        HTML;
        $expect_html = <<<HTML
            <h1><?=\$Blog->title?></h1>
        HTML;


        $var_exports = 
        [
            'DataNodes'=>
            [
                ['type'=>'default']
            ],
        ];

        $items_data = [];

        $doc = $this->run_parse_test($view, $expect_html, $var_exports, $items_data);

    }

    public function testDataItemClean(){
        $view = <<<HTML
            <div item="Blog">
                <p-data where="Blog.id = :id"></p-data>
                <h1 prop="title"></h1>
                <p prop="intro"></p>
            </div>
        HTML;
        $expect_html = <<<HTML
            <div>
                <h1><?=\$Blog->title?></h1>
                <p><?=\$Blog->intro?></p>
            </div>
        HTML;


        $var_exports = 
        [
            'DataNodes'=>
            [
                ['where'=>'Blog.id = :id', 'type'=>'node'],
                ['type'=>'default']
            ],
        ];

        $items_data = [];

        $this->run_parse_test($view, $expect_html, $var_exports, $items_data);
    }

    public function testDataItem(){
        $parser = new \Phad\DomParser();
        $view = <<<HTML
            <div item="Blog">
                <p-data where="Blog.id = :id"></p-data>
                <h1 prop="title"></h1>
                <p prop="intro"></p>
            </div>
        HTML;
        $expect_html = <<<HTML
            <div>

                <h1><?=\$Blog->title?></h1>
                <p><?=\$Blog->intro?></p>
            </div>
        HTML;
        $doc = new \Taeluf\PHTML($view);

        $items = $parser->parse_doc($doc);

        $this->compare_lines(
            $expect_html,
            $items[0]['html_code']
        );
        unset($items[0]['html_code']);
        $this->compare_arrays(
            [
                ['item_name'=>'Blog',
                'item_type'=>'view',
                'apis'=>var_export(['view', 'data'], true),
                'DataNodes'=>var_export(
                    [
                        ['where'=>'Blog.id = :id', 'type'=>'node'],
                        ['type'=>'default']
                    ] ,true),
                'ItemForeach' => 'Blog => $BlogRow',

                'ItemName' => '$Blog',
                'Item_Row' => '$BlogRow',
                'ItemInfo' => '$BlogInfo',
                'ItemInfoSubmitErrorsList' => 'BlogSubmitErrorsList',
                'route_info' => '',
                'sitemap_info' => '',
                'form_properties' => '',
                'form_is_candelete' => '',
                // 'item_rows' => '',
                // 'response_code_200' => '',
                'status_codes'=>'',
                'response_code_403' => '',
                'response_code_404' => '',
                'response_code_500' => '',
                'form_submit' => '',
                // I changed phad to ALWAYS print the can_read_row() code, so I lazily added this to make the test pass. 
                'can_read_row' => $items[0]['can_read_row'],//'',
                // 'html_code' => trim($expect_html),
                ]
            ],

            $items
        );

    }

    public function testSimpleItem(){
        $parser = new \Phad\DomParser();
        $view = <<<HTML
            <div item="Blog">
                <h1 prop="title"></h1>
                <p prop="intro"></p>
            </div>
        HTML;
        $expect_html = <<<HTML
            <div>
                <h1><?=\$Blog->title?></h1>
                <p><?=\$Blog->intro?></p>
            </div>
        HTML;
        $doc = new \Taeluf\PHTML($view);

        $items = $parser->parse_doc($doc);

        $this->compare_arrays(
            [
                ['item_name'=>'Blog',
                'item_type'=>'view',
                'apis'=>var_export(['view', 'data'], true),
                'DataNodes'=>var_export([['type'=>'default']] ,true),
                'ItemForeach' => 'Blog => $BlogRow',

                'ItemName' => '$Blog',
                'Item_Row' => '$BlogRow',
                'ItemInfo' => '$BlogInfo',
                'ItemInfoSubmitErrorsList'=> 'BlogSubmitErrorsList',
                'route_info' => '',
                'sitemap_info' => '',
                'form_properties' => '',
                'form_is_candelete' => '',
                // 'item_rows' => '',
                // 'response_code_200' => '',
                'status_codes' => '',
                'response_code_403' => '',
                'response_code_404' => '',
                'response_code_500' => '',
                'form_submit' => '',
                // I changed phad to ALWAYS print the can_read_row() code, so I lazily added this to make the test pass. 
                'can_read_row' => $items[0]['can_read_row'],
                'html_code' => trim($expect_html),
                ]
            ],

            $items
        );


    }

}